# exception & assert

# 错误和异常

  • 一般程序的错误包括至少两种,一种是语法错误,一种是异常。

  • 语法错误是指代码不符合 Python 的语法规范,程序会报错invalid syntax。

  • 而异常则是指程序的语法正确,也可以被执行,但在执行过程中遇到了错误,抛出了异常。

    • 一个 try 语句可能包含多个except子句,分别来处理不同的特定的异常。最多只有一个分支会被执行。
    • 一个except子句可以同时处理多个异常,这些异常将被放在一个括号里成为一个元组
    • try except 语句一个可选的else子句,如果使用这个子句,那么必须放在所有的except子句之后。这个子句将在try子句没有发生任何异常的时候执行。
    • try except 语句还有另外一个可选的finally子句,它定义了无论在任何情况下都会执行的清理行为。
  • 当我们不确定某段代码能否执行成功,往往这个地方就需要使用异常处理

  • 异常不要滥用,有的地方用流程控制即可

import sys
try:
    f = open('file.txt', 'r')
    .... # some data processing
except OSError as err:
    print('OS error: {}'.format(err))
except:
    print('Unexpected error:', sys.exc_info()[0])
else: # 没有发生任何异常
    pass
finally: # 清理行为,无论如何都会执行
    f.close()

# 自定义异常

  • python允许用户自定义异常,直接间接继承自 Exception 类即可。
  • 使用raise抛出一个指定的异常(Exception 的子类)
class MyInputError(Exception):
    """Exception raised when there're errors in input"""
    def __init__(self, value): # 自定义异常类型的初始化
        self.value = value
    def __str__(self): # 自定义异常类型的string表达形式
        return ("{} is invalid input".format(repr(self.value)))
  
try:
    raise MyInputError(1) # 抛出MyInputError这个异常
except MyInputError as err:
    print('error: {}'.format(err))

# 异常响应链- raise from

在设计代码的时候,对于在except块中使用raise语句的情况,大家应该特别小心。大部分情况下,这种raise语句都应该改为raise from。也就是说,我们应该采用下面这种风格:

try:
    ...
except SomeException as e:
    raise DifferentException() from e

这么做的原因在于我们需要显式将异常产生的原因串联起来。也就是说,Different Exception是直接响应SomeException。这两个异常间的关系会在traceback回溯中显式给出。

如果采用下面这种风格,还是可以得到异常链。但是通常这么做不能明确表达出异常链是程序员有意为之,还是由于无法预见的编程错误而产生的:

try:
   ...
except SomeException:
   raise DifferentException()

当使用raise from语句时,就需明确表达出你希望引发第二个异常的意图。示例如下:

>>> def fun2():         
...     try:
...             int('N/A')
...     except ValueError as e:
...             raise RuntimeError('A parsing error occurred') from e 
...
>>> fun2()  
Traceback (most recent call last):
  File "<stdin>", line 3, in fun2
ValueError: invalid literal for int() with base 10: 'N/A'

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 5, in fun2
RuntimeError: A parsing error occurred

当在except语句块中引发另一个异常时,会产生异常链的隐式形式,但是会产生上面提到的问题-即不知道是有意抛出还是未知的编程错误:

>>> def example2():
...     try:
...         int('N/A')
...     except ValueError as e:
...         print("Couldn't parse:", err)
...
>>>
>>> example2()
Traceback (most recent call last):
  File "<stdin>", line 3, in example2
ValueError: invalid literal for int() with base 10: 'N/A'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 5, in example2
NameError: global name 'err' is not defined

raise form None​则会隐藏被触发的异常。

>>> def fun2():
...     try:
...             int('N/A')
...     except ValueError as e:
...             raise RuntimeError('A parsing error occurred') from None 
...
>>> fun2()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 5, in fun2
RuntimeError: A parsing error occurred

# __cause__

可以根据Exception的__cause__​属性来获取被触发的原因,其指向了另一个导致了当前正在处理的Exception的Exception对象,在调用链中会被自动设置:

def divide(x, y):
    try:
        result = x / y
    except ZeroDivisionError as e:
        raise ValueError('Invalid inputs') from e
    else:
        return result

try:
    divide(5, 0)
except ValueError as e:
    print("Cause of error:", e.__cause__)
    print(type(e.__cause__))

# Cause of error: division by zero
# <class 'ZeroDivisionError'>

# assert

assert 在python中是一个关键字,用于判断一个表达式,在表达式为False的时候抛出AssertionError异常, 后接optional的message, 例如:

assert 1 != 1, '1 != 1 is not True'

从某种程度上来说其和exception有点类似,但是assert的使用场景和exception是不同的, 这需要很多的工程经验以及一定的代码美感才能做到,这里不展开讲。

需要注意:assert语句在python执行时如果开启-O​ 是会被优化掉的-直接忽略.

个人认为assert的使用应该是,有没有assert程序都能够正常运行,但有了assert可以使我们的代码后期维护更加方便